Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/next/pages/vouchers/[id].tsx
1450 views
1
/*
2
* This file is part of CoCalc: Copyright © 2023 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { useEffect, useMemo, useState } from "react";
7
import Footer from "components/landing/footer";
8
import Header from "components/landing/header";
9
import Head from "components/landing/head";
10
import {
11
Alert,
12
Button,
13
Card,
14
Divider,
15
Layout,
16
Modal,
17
Space,
18
Table,
19
} from "antd";
20
import withCustomize from "lib/with-customize";
21
import { Customize } from "lib/customize";
22
import { Icon } from "@cocalc/frontend/components/icon";
23
import A from "components/misc/A";
24
import Loading from "components/share/loading";
25
import TimeAgo from "timeago-react";
26
import apiPost from "lib/api/post";
27
import Avatar from "components/account/avatar";
28
import type { VoucherCode } from "@cocalc/util/db-schema/vouchers";
29
import { stringify as csvStringify } from "csv-stringify/sync";
30
import { currency, human_readable_size } from "@cocalc/util/misc";
31
import CodeMirror from "components/share/codemirror";
32
import { trunc } from "lib/share/util";
33
import useDatabase from "lib/hooks/database";
34
import Notes from "./notes";
35
import Help from "components/vouchers/help";
36
import Copyable from "components/misc/copyable";
37
38
function RedeemURL({ code }) {
39
const [url, setUrl] = useState<string>("");
40
useEffect(() => {
41
if (typeof window !== "undefined") {
42
setUrl(codeToUrl(code, window.location.href));
43
}
44
}, []);
45
46
return (
47
<Space>
48
<A href={url}>
49
<Icon name="external-link" />
50
</A>{" "}
51
<Copyable display={`…${code}`} value={url} />
52
</Space>
53
);
54
}
55
56
const COLUMNS = [
57
{
58
title: "Redeem URL (share this)",
59
dataIndex: "url",
60
key: "redeem",
61
render: (_, { code }) => <RedeemURL code={code} />,
62
},
63
{
64
title: "Code",
65
dataIndex: "code",
66
key: "code",
67
},
68
{
69
title: "Created",
70
dataIndex: "created",
71
key: "created",
72
align: "center",
73
render: (_, { created }) => (
74
<>{created == null ? "-" : <TimeAgo datetime={created} />}</>
75
),
76
},
77
{
78
title: "When Redeemed",
79
dataIndex: "when_redeemed",
80
key: "when_redeemed",
81
align: "center",
82
render: (_, { when_redeemed }) => (
83
<>{when_redeemed == null ? "-" : <TimeAgo datetime={when_redeemed} />}</>
84
),
85
},
86
{
87
title: "Redeemed By",
88
dataIndex: "redeemed_by",
89
key: "redeemed_by",
90
align: "center",
91
render: (_, { redeemed_by }) => (
92
<>{redeemed_by ? <Avatar account_id={redeemed_by} /> : undefined}</>
93
),
94
},
95
96
{
97
title: "Canceled",
98
dataIndex: "canceled",
99
key: "canceled",
100
align: "center",
101
render: (_, { canceled }) => (canceled ? "Yes" : "-"),
102
},
103
{
104
title: "Your Private Notes",
105
dataIndex: "notes",
106
key: "notes",
107
render: (_, { notes, code }) => <Notes notes={notes} code={code} />,
108
},
109
] as any;
110
111
type DownloadType = "csv" | "json";
112
113
export default function VoucherCodes({ customize, id }) {
114
const database = useDatabase({ vouchers: { id, title: null, cost: null } });
115
const [error, setError] = useState<string>("");
116
const [loading, setLoading] = useState<boolean>(true);
117
const [data, setData] = useState<VoucherCode[] | null>(null);
118
const [showModal, setShowModal] = useState<DownloadType | null>(null);
119
120
useEffect(() => {
121
setLoading(true);
122
(async () => {
123
try {
124
const { codes } = await apiPost("/vouchers/get-voucher-codes", { id });
125
setData(codes);
126
} catch (err) {
127
setError(`${err}`);
128
} finally {
129
setLoading(false);
130
}
131
})();
132
}, []);
133
134
const allCodes = useMemo(() => {
135
if (!data) return [];
136
return data.map((x) => x.code);
137
}, [data]);
138
139
const unusedCodes = useMemo(() => {
140
if (!data) return [];
141
return data.filter((x) => !x.when_redeemed).map((x) => x.code);
142
}, [data]);
143
144
const usedCodes = useMemo(() => {
145
if (!data) return [];
146
return data.filter((x) => !!x.when_redeemed).map((x) => x.code);
147
}, [data]);
148
149
return (
150
<Customize value={customize}>
151
<Head title={`Voucher With id=${id}`} />
152
<DownloadModal
153
data={data}
154
id={id}
155
type={showModal}
156
onClose={() => setShowModal(null)}
157
/>
158
<Layout>
159
<Header />
160
<Layout.Content>
161
<div
162
style={{
163
width: "100%",
164
margin: "10vh 0",
165
display: "flex",
166
justifyContent: "center",
167
}}
168
>
169
<Card style={{ background: "#fafafa" }}>
170
<Space direction="vertical" align="center">
171
<A href="/vouchers">
172
<Icon name="gift2" style={{ fontSize: "75px" }} />
173
</A>
174
<h1>Voucher: id={id}</h1>
175
{database.value?.vouchers?.title && (
176
<h3>Title: {database.value.vouchers.title}</h3>
177
)}
178
{database.value?.vouchers != null && (
179
<div
180
style={{
181
margin: "auto",
182
padding: "15px",
183
textAlign: "center",
184
fontSize: "14pt",
185
}}
186
>
187
Each Voucher is Worth{" "}
188
{currency(database.value?.vouchers?.cost)} in credit.
189
</div>
190
)}
191
<Divider />
192
193
{error && (
194
<Alert
195
type="error"
196
message={error}
197
showIcon
198
style={{ width: "100%", marginBottom: "30px" }}
199
closable
200
onClose={() => setError("")}
201
/>
202
)}
203
{loading && <Loading />}
204
{!loading && data && (
205
<div>
206
<div
207
style={{
208
display: "flex",
209
justifyContent: "center",
210
marginBottom: "15px",
211
}}
212
>
213
<Space direction="vertical">
214
<Space>
215
<div style={{ width: "200px" }}>
216
Copy All Codes {`(${allCodes.length})`}
217
</div>
218
<Copyable
219
value={allCodes.join(", ")}
220
inputWidth={"200px"}
221
/>
222
</Space>
223
<Space>
224
<div style={{ width: "200px" }}>
225
Copy Unused Codes {`(${unusedCodes.length})`}
226
</div>
227
<Copyable
228
value={unusedCodes.join(", ")}
229
inputWidth={"200px"}
230
/>
231
</Space>
232
<Space>
233
<div style={{ width: "200px" }}>
234
Copy Redeemed Codes {`(${usedCodes.length})`}
235
</div>
236
<Copyable
237
value={usedCodes.join(", ")}
238
inputWidth={"200px"}
239
/>
240
</Space>
241
<Space>
242
<div style={{ width: "200px" }}>
243
Export all data to CSV
244
</div>
245
<Button onClick={() => setShowModal("csv")}>
246
<Icon name="csv" /> Export to CSV...
247
</Button>
248
</Space>
249
<Space>
250
<div style={{ width: "200px" }}>
251
Export all data to JSON
252
</div>
253
<Button onClick={() => setShowModal("json")}>
254
<Icon name="js-square" /> Export to JSON...
255
</Button>
256
</Space>
257
</Space>
258
</div>
259
260
<Table
261
columns={COLUMNS}
262
dataSource={data}
263
rowKey="code"
264
pagination={{ defaultPageSize: 50 }}
265
/>
266
</div>
267
)}
268
{!loading && data?.length == 0 && (
269
<div>
270
You have not <A href="/redeem">redeemed any vouchers</A>{" "}
271
yet.
272
</div>
273
)}
274
<Help />
275
</Space>
276
</Card>
277
</div>
278
<Footer />
279
</Layout.Content>
280
</Layout>
281
</Customize>
282
);
283
}
284
285
export async function getServerSideProps(context) {
286
const { id } = context.params;
287
return await withCustomize({ context, props: { id } });
288
}
289
290
function DownloadModal({ type, data, id, onClose }) {
291
const [data0, setData0] = useState<VoucherCode[] | null>(data);
292
useEffect(() => {
293
if (data == null) return;
294
if (typeof window == "undefined") return;
295
setData0(
296
data.map((x) => {
297
return { ...x, url: codeToUrl(x.code, window.location.href) };
298
}),
299
);
300
}, [data]);
301
const path = `vouchers-${id}.${type}`;
302
const content = useMemo(() => {
303
if (!type || data0 == null) return "";
304
if (type == "csv") {
305
const x = [COLUMNS.map((x) => x.title)].concat(
306
data0.map((x) => COLUMNS.map((c) => x[c.dataIndex])),
307
);
308
return csvStringify(x);
309
} else if (type == "json") {
310
return JSON.stringify(data0, undefined, 2);
311
}
312
return "";
313
}, [type, data0]);
314
315
const body = useMemo(() => {
316
if (!type || !data) {
317
return null;
318
}
319
return (
320
<div>
321
<div style={{ margin: "30px", fontSize: "13pt", textAlign: "center" }}>
322
<a
323
href={URL.createObjectURL(
324
new Blob([content], { type: "text/plain" }),
325
)}
326
download={path}
327
>
328
Download {path} (size: {human_readable_size(content.length)})
329
</a>
330
</div>
331
<CodeMirror
332
lineNumbers={false}
333
content={trunc(content, 500)}
334
filename={path}
335
/>
336
</div>
337
);
338
}, [type, data, id]);
339
340
return (
341
<Modal
342
open={type != null}
343
onCancel={onClose}
344
onOk={onClose}
345
title={<>Export all data to {type ? type.toUpperCase() : ""}</>}
346
>
347
{body}
348
</Modal>
349
);
350
}
351
352
function codeToUrl(code, href): string {
353
let i = href.lastIndexOf("/");
354
i = href.lastIndexOf("/", i - 1);
355
return `${href.slice(0, i)}/redeem/${code}`;
356
}
357
358